package org.xx.armory.netty;

import io.netty.buffer.ByteBufOutputStream;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.util.AsciiString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.commons.StreamWriter;
import org.xx.armory.rpc.ResponseException;
import org.xx.armory.rpc.RpcCookie;
import org.xx.armory.rpc.RpcResponse;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;

import static io.netty.channel.ChannelFutureListener.CLOSE;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.util.ReferenceCountUtil.release;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.xx.armory.commons.Validators.greaterThanOrEqual;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 用于channel的RPC响应实现。
 *
 * @author Haart
 */
final class ChannelRpcResponse
        implements RpcResponse {
    private static final AsciiString CONNECTION = AsciiString.of("Connection");
    private static final AsciiString CONTENT_LENGTH = AsciiString.of("Content-Length");
    private static final AsciiString CONTENT_TYPE = AsciiString.of("Content-Type");
    private static final AsciiString SET_COOKIE = AsciiString.of("Set-Cookie");
    private static final AsciiString DATE = AsciiString.of("Date");
    private static final AsciiString KEEP_ALIVE = AsciiString.of("keep-alive");
    private static final AsciiString LAST_MODIFIED = AsciiString.of("Last-Modified");

    private final Logger logger = LoggerFactory.getLogger(ChannelRpcResponse.class);
    private final ChannelHandlerContext ctx;
    private final FullHttpResponse httpResponse;
    private final Collection<RpcCookie> cookies;
    private final boolean keepAlive;

    /**
     * 构造{@code ChannelRpcResponse}对象的实例。
     *
     * @param context
     *         Channel处理器上下文。
     * @param httpResponse
     *         HTTP响应对象。
     * @param keepAlive
     *         是否保持连接。
     * @throws IllegalArgumentException
     *         如果参数{@code ctx}或者{@code httpResponse}是{@code null}。
     */
    ChannelRpcResponse(
            FullHttpResponse httpResponse,
            ChannelHandlerContext context,
            boolean keepAlive
    ) {
        this.ctx = notNull(context, "ctx");
        this.httpResponse = notNull(httpResponse, "httpResponse").retain();
        this.cookies = new LinkedHashSet<>();
        this.keepAlive = keepAlive;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setContentSize(
            long contentSize
    ) {
        greaterThanOrEqual(contentSize, "contentSize", 0L);
        httpResponse.headers().add(CONTENT_LENGTH, contentSize);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setContentType(
            String contentType,
            Charset charset
    ) {
        contentType = notBlank(contentType, "contentType").trim();
        if (charset != null) {
            contentType = contentType + ";charset=" + charset.name();
        }
        httpResponse.headers().add(CONTENT_TYPE, contentType);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDate(
            Date date
    ) {
        httpResponse.headers().add(DATE, new Date());
        if (date != null) {
            httpResponse.headers().add(LAST_MODIFIED, date);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<RpcCookie> getCookies() {
        return this.cookies;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void ok(
            StreamWriter writer
    ) {
        httpResponse.setStatus(OK);

        for (final RpcCookie cookie : cookies) {
            final Cookie pc = new DefaultCookie(cookie.getName(), cookie.getValue());
            if (cookie.getMaxAge() >= 0) {
                pc.setMaxAge(cookie.getMaxAge());
            } else {
                pc.setMaxAge(Cookie.UNDEFINED_MAX_AGE);
            }
            if (!cookie.getDomain().isEmpty()) {
                pc.setDomain(cookie.getDomain());
            }
            pc.setPath(cookie.getPath());

            final String cookieString = ServerCookieEncoder.STRICT.encode(pc);
            this.httpResponse.headers().add(SET_COOKIE, cookieString);
        }

        if (keepAlive) {
            httpResponse.headers().add(CONNECTION, KEEP_ALIVE);
        }

        try (final OutputStream out = new ByteBufOutputStream(httpResponse.content())) {
            writer.writeTo(out);
            out.flush();
        } catch (IOException ex) {
            throw new ResponseException(ex);
        }

        writeAndFlush();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void badRequest() {
        this.httpResponse.setStatus(BAD_REQUEST);
        this.httpResponse.headers().add(CONTENT_LENGTH, 0);
        writeAndFlush();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void notFound() {
        this.httpResponse.setStatus(NOT_FOUND);
        this.httpResponse.headers().add(CONTENT_LENGTH, 0);
        writeAndFlush();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void accessDenied() {
        this.httpResponse.setStatus(FORBIDDEN);
        this.httpResponse.headers().add(CONTENT_LENGTH, 0);
        writeAndFlush();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void exception(
            Throwable t
    ) {
        final byte[] bytes;
        if (t != null) {
            bytes = encodeUtf8(t.getLocalizedMessage());
        } else {
            bytes = new byte[0];
        }
        this.httpResponse.setStatus(INTERNAL_SERVER_ERROR);
        this.httpResponse.headers().add(CONTENT_TYPE, "text/plain;charset=utf-8");
        this.httpResponse.headers().add(CONTENT_LENGTH, bytes.length);
        this.httpResponse.content().writeBytes(bytes);
        writeAndFlush();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void abandon(
            Throwable t
    ) {
        if (t != null) {
            logger.error("httpResponse was abandoned", t);
        }
        this.ctx.fireExceptionCaught(t);
    }

    /**
     * 输出并刷新HTTP响应。
     * <p>
     * 如果不需要保持连接，那么刷新后关闭连接，否则仅仅刷新。
     * </p>
     */
    private void writeAndFlush() {
        if (logger.isTraceEnabled()) {
            final StringBuilder buffer1 = new StringBuilder("httpResponse headers:\n");
            for (final String name : httpResponse.headers().names()) {
                buffer1.append(String.format("> %s:%s\n", name, httpResponse.headers().get(name)));
            }
            logger.trace(buffer1.toString());
        }

        if (this.ctx.executor().inEventLoop()) {
            doFlush();
        } else {
            this.ctx.executor().execute(this::doFlush);
        }
    }

    private byte[] encodeUtf8(
            String s
    ) {
        if (s != null) {
            return s.getBytes(UTF_8);
        } else {
            return new byte[0];
        }
    }

    private void doFlush() {
        final ChannelFuture cf = this.ctx.writeAndFlush(this.httpResponse);
        if (!this.keepAlive) {
            cf.addListener(CLOSE);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close()
            throws Exception {
        release(this.httpResponse);
    }

}
